/*
 * Copyright (C) 2009 Leandro Aparecido <lehphyro@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.metalframework.persistence.repository;

import java.beans.*;
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;

import org.metalframework.persistence.annotation.*;
import org.springframework.beans.*;
import org.springframework.core.*;
import org.springframework.util.*;


/**
 * Base class for parsers of annotations specifying repository queries.
 * 
 * @since 1.0
 * @see org.metalframework.persistence.repository.query.RepositoryQueryParser
 * @see org.metalframework.persistence.repository.ret.RepositoryReturnParser
 * @see org.springframework.core.ParameterNameDiscoverer
 */
public class RepositoryAnnotationParser {
	/**
	 * Discoverer of method parameter names.
	 */
	private ParameterNameDiscoverer parameterNameDiscoverer;

	/**
	 * Creates a parser using the {@link org.springframework.core.LocalVariableTableParameterNameDiscoverer}
	 * to discover method parameter names.
	 */
	public RepositoryAnnotationParser() {
		parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
	}
	
	/**
	 * Parse the query name from the value and name attributes of the annotation.
	 * Makes sure only one of the two values is defined.
	 * 
	 * @param value Value attribute of the annotation. 
	 * @param name Name attribute of the annotation.
	 * @return Name of the query.
	 * @see org.metalframework.persistence.annotation.RepositoryQuery#value()
	 * @see org.metalframework.persistence.annotation.RepositoryQuery#name()
	 */
	protected String parseQueryName(String value, String name) {
		String queryName;
		
		if (StringUtils.hasText(value) && StringUtils.hasText(name)) {
			throw new IllegalArgumentException("Only one attribute can be defined for RepositoryQuery/Return annotation: value or name");
		}
		
		if (StringUtils.hasText(value)) {
			queryName = value;
		} else if (StringUtils.hasText(name)) {
			queryName = name;
		} else {
			queryName = null;
		}
		
		return queryName;
	}

	/**
	 * Parse the query parameters using the {@link #parameterNameDiscoverer}.
	 * 
	 * @param method Method to parse the parameters.
	 * @param arguments Arguments passed to the method invocation.
	 * @return Map containing the associations name => value.
	 */
	protected Map<String, QueryParameter> parseQueryParameters(Method method, Object[] arguments) {
		Map<String, QueryParameter> parameters = new HashMap<String, QueryParameter>();
		
		String[] names = parameterNameDiscoverer.getParameterNames(method);
		if (names == null) {
			if (method.getParameterTypes().length > 0) {
				throw new UnsupportedOperationException("Could not discover parameter names of method [" + method + "]. To do this, you have to compile your classes with debug information or configure argument names on RepositoryQuery annotations");
			}
		} else {
			for (int i = 0; i < arguments.length; i++) {
				parameters.put(names[i], parseQueryParameter(names[i], arguments[i], method.getParameterAnnotations()[i]));
			}
		}
		
		return parameters;
	}

	/**
	 * Parse query parameters investigating the javabean-style properties of the passed method and
	 * arguments.
	 * 
	 * @param method Method to parse the parameters.
	 * @param arguments Arguments passed to the method invocation.
	 * @param index Index of the argument to investigate the javabean properties, all the other
	 * arguments will be parsed using {@link #parseQueryParameters(Method, Object[])}.
	 * @return Map containing the associations name => value.
	 */
	protected Map<String, QueryParameter> parseQueryParametersByExample(Method method, Object[] arguments, int index) {
		Map<String, QueryParameter> parameters = new HashMap<String, QueryParameter>();
		
		Object exampleBean = arguments[index];
		if (exampleBean == null) {
			throw new IllegalArgumentException("Cannot use null example in query by example of method [" + method + "] argument [" + index + "]");
		}
		
		// Method parameters should be available too
		parameters.putAll(parseQueryParameters(method, arguments));
		
		// Parameters from the example bean
		parameters.putAll(parseQueryParametersFromExampleBean(exampleBean));
		
		return parameters;
	}
	
	/**
	 * Parse query parameters investigating the javabean-style properties of the passed object.
	 * 
	 * @param exampleBean Object to investigate.
	 * @return Map containing the associations name => parameter.
	 */
	protected Map<String, QueryParameter> parseQueryParametersFromExampleBean(Object exampleBean) {
		Map<String, QueryParameter> parameters = new HashMap<String, QueryParameter>();
		
		// Parameters from the example bean
		BeanWrapper beanWrapper = new BeanWrapperImpl(exampleBean);
		for (PropertyDescriptor propertyDescriptor : beanWrapper.getPropertyDescriptors()) {
			String propertyName = propertyDescriptor.getName();
			parameters.put(propertyName, new QueryParameter(propertyName, beanWrapper.getPropertyValue(propertyName)));
		}
		
		return parameters;
	}

	/**
	 * Parse parameters for the query.
	 * 
	 * @param name
	 * @param value
	 * @param parameterAnnotations
	 * @return
	 */
	protected QueryParameter parseQueryParameter(String name, Object value, Annotation[] parameterAnnotations) {
		RepositoryQueryParam annotation = null;
		for (Annotation parameterAnnotation : parameterAnnotations) {
			if (RepositoryQueryParam.class == parameterAnnotation.annotationType()) {
				annotation = (RepositoryQueryParam)parameterAnnotation;
			}
		}
		
		if (annotation == null) {
			return new QueryParameter(name, value);
		}
		
		if (annotation.forLike() &&
			(StringUtils.hasText(annotation.prefix()) ||
			 StringUtils.hasText(annotation.suffix()))) {
			throw new IllegalArgumentException("Repository query parameter cannot be forLike and define prefix or suffix at the same time");
		}
		
		if (annotation.forLike()) {
			return new QueryParameter(name, value, true);
		}
		
		return new QueryParameter(name, value, annotation.prefix(), annotation.suffix());
	}

// Getters/Setters
	
	public ParameterNameDiscoverer getParameterNameDiscoverer() {
		return parameterNameDiscoverer;
	}
	
	public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
		this.parameterNameDiscoverer = parameterNameDiscoverer;
	}
}
